pytest+motoでS3へのバケット一覧リクエストからオブジェクト確認リクエストまで細かくテストしてみた
はじめに
Python製のロジックに対するユニットテストにはpytestは有効な一打です。そして、AWSサービスへアクセスするロジックに対するテストにはmotoという選択肢があります。
一例として、S3のファイル有無確認用ロジックのテストコードについて、motoを使って試行錯誤した末のまとめを書いてみました。
テストを組み立てる順番
今回は以下の順番で進めます。
- テスト環境の作成
- テストケースの追加
- クラス実装
- テストの実行
前提環境
- Python3.7
依存ライブラリ
- pipenv
- moto
- boto3
- pytest
テスト環境の作成
pipenv install --python 3.7 pipenv install boto3 pipenv install --dev moto pytest
ファイル・ディレクトリ構成
ユニットテスト実行を目的として、以下の構成にしました。各__init__.py
は空のファイルです。
setup.cfg src/ mypkg/ __init__.py s3_access.py tests/ __init__.py foo/ __init__.py conftest.py test_s3_access.py
setup.cfgの作成
pytest
のコマンドだけで引数レス実行できるように指定します。
- testpaths
- テスト対象ディレクトリ
- python_files
- 対象ファイルパターン
- python_functions
- 対象テスト名パターン(指定されたパターンから始まるテストのみ
[tool:pytest] testpaths = tests/ python_files = test_*.py python_functions = test minversion = 3.7 addpot = --pdb
tests/foo/conftest.pyの作成
テストケースから実装クラス呼び出しを行うために、src/
ディレクトリへのパスを追加します。
from pathlib import Path import sys sys.path.append(str(Path("./src/").resolve()))
テストケースの追加
pytestとmotoを併用して実装します。
motoを利用したテストは、必要に応じてテストケース用データを作成する必要があります。S3の場合、データの作成は@mock_s3
デコレータを利用した関数内で、S3へのリクエストコードを追加・実行することによって行います。このデータは一時的なものであるため、必要に応じてテスト毎に作成します。
例として、bucketの作成前後を確認した手続きは以下のようになります。2回目のlist_buckets()
にて、テストケース用データが空ではなくなったことを確認しています。
# S3上にbucketが作成されていないことを確認 % aws s3 ls | grep 'test_bucket' % vim test_bucket.py import boto3 from moto import mock_s3 @mock_s3 def test_s3_bucket(): client = boto3.client('s3') assert client.list_buckets()['Buckets'] == [] client.create_bucket(Bucket='test_bucket') assert client.list_buckets()['Buckets'] == [] % pytest test_bucket.py .. .. ========================== FAILURES ==================== _______________________ test_s3_bucket ________________ .. .. @mock_s3 def test_s3_bucket(): client = boto3.client('s3') assert client.list_buckets()['Buckets'] == [] client.create_bucket(Bucket='test_bucket') > assert client.list_buckets()['Buckets'] == [] E AssertionError: assert [{'CreationDa...test_bucket'}] == [] E Left contains one more item: {'CreationDate': datetime.datetime(2006, 2, 3, 16, 45, 9, tzinfo=tzutc()), 'Name': 'test_bucket'} E Use -v to get the full diff # 実際にはS3上でbucketが作成されていないことを確認 % aws s3 ls | grep 'test_bucket'
ファイル有無確認用のテストコードを作成する
気をつけるべき点として、実際にはアップロードされないとしても、upload_file
で対象にするファイルは必ずローカルに存在しなければなりません。
% touch /path/to/file
tests/foo/test_s3_access.pyの作成
from moto import mock_s3 import boto3 import os import pytest import .mypkg.s3_access import S3Download @mock_s3 def test_download(): BUCKET = 'bucket' FILE_PATH = '/path/to/file' FILE_NAME = 'file' client = boto3.client('s3') client.create_bucket(Bucket=BUCKET) client.upload_file(FILE_PATH, BUCKET, FILE_NAME) s3_downloader = S3Download() assert s3_downloader.isexists(FILE_NAME) is True
クラスの実装
S3上のオブジェクト確認にはhead_object
を利用しました。
S3 — Boto 3 Docs 1.9.170 documentation
src/mypkg/s3_access.pyの作成
import boto3 from botocore.errorfactory import ClientError class S3Download: def isexists(self, file_name): try: boto3.client('s3').head_object(Bucket='bucket', Key=file_name) except ClientError: return False return True
テストの実行
今回pytest
のみで実行可能にしているため、引数は不要です。
% pytest ============ test session starts ================ .. .. collected 1 item test_bucket.py .
まとめ
「AWS上のサービスを操作するロジックのテストコードは書き辛い」と思った時こそ、motoの利用をおすすめします。